# -*- coding: utf-8 -*-
"""
*** DO NOT MODIFY ANYTHING IN THIS SCRIPT UNLESS YOU KNOW WHAT YOU ARE DOING ***

This script allows for easy banning of a user.  Once banned, the user can
be un-banned from the web-application as the ban is stored inside of a mysql
database.  Also, if the user attempts to join the server, they will get auto-kicked
as it will do a check against the mysql database to see if they exist and if
their ban period has yet to expire.  When the user has been banned on once server,
the user will be banned on all servers that are stored within the mysql table
that contains the servers that are running.  The script must be intalled in all
servers and the server ID must match the ID inside of the servers mysql table.


To ban, simply type !banmenu in say mode.  This will first let you select a
ban reason, followed by a ban length, and then finally the user to ban.  The
command is hidden from say and can not be seen by the public and is only
accessible by those found in the clan_db script.


For developers that wish to integrate GlobalBan into their script, please call
the regcmd gb_externalBanUser.  To execute it, do the following from your script:
es gb_externalBanUser <adminUserId> <steamIdToBan> <reasonIdOfBan> <lengthOfBan> <timeScale>
The following is an example:
The admin with userid of 432 is banning the user with Steam ID STEAM_0:0:111111 for reason id 1 forever
Timescale can be in minutes, hours, days, weeks, or months
es gb_externalBanuser 432 STEAM_0:0:111111 1 0 minutes


The following MUST be placed inside of your autoexec.cfg file.
es_load GlobalBan


Special Thanks to: ub|Delta One - http://www.urbanbushido.net/
                   tnbporsche911 - http://www.tnbsourceclan.net/
                   arjuna - helped with threading

Requirements:
PHP 4 or 5
MySQL 4.1 or 5
EventScripts v2.0+
"""

import es
import playerlib
import os.path
import urllib2
import keymenulib
import gamethread
import threading
import Queue

#plugin information
info = es.AddonInfo()
info.name = "GlobalBan"
info.version = "3.2"
info.author = "Soynuts"
info.url = "http://www.unbuinc.net"
info.description = "Bans are stored in a mySQL database on a webserver"
info.basename = "GlobalBan"

# Default config values
serverId = 1
websiteAddy = "http://yourdomain.com/banned/"
banAppealMessage = "Banned. Appeal at yourdomain.com"
hashCode = "secretHashCode"
teachAdmins = 1
clanName = "Your Clan Name or Community"
allowAdminBanning = 0

################################################################################
# DO NOT EDIT BELOW HERE UNLESS YOU KNOW WHAT YOU ARE DOING
################################################################################

# Class for handling connections
class runUrl(threading.Thread):
  def run(self):
    # Get the next url to check
    urlToCheck = urlQueue.get()
    if urlToCheck != None:
      f = urllib2.urlopen(urlToCheck)
      data = f.read()
      data = data.strip() # remove whitespace
      f.close()
      es.log('URL READ: ' + urlToCheck)
      # If data is returned, it will be a kick command
      # It will be empty data if no matching ban is found or if it is a ban add
      if data != "":
        es.log('Found banned user, executing kick!')
        es.server.cmd('es ' + data);
        es.log('es ' + data)

# Class that will read the admin list from the web site
class AdminList(threading.Thread):
  def run(self):
    f = urllib2.urlopen(websiteAddy + "index.php?page=getAdminList&es=1")
    data = f.read()
    data = data.strip() # remove any whitespace
    f.close()
    es.log('Done getting admin list')

    # Open the clanMembers db file to write to
    f = open(es.getAddonPath('GlobalBan') + '/es_clanMembers_db.txt', 'w')
    f.write(data)
    f.close()
    
# Class that will read the ban reason list from the web site
class BanReasonList(threading.Thread):
  def run(self):
    f = urllib2.urlopen(websiteAddy + "index.php?page=getBanReasonList&es=1")
    data = f.read()
    data = data.strip() # remove any whitespace
    f.close()
    es.log('Done getting Ban Reason list')

    # Open the es_GlobalBan_Reason_db.txt db file to write to
    f = open(es.getAddonPath('GlobalBan') + '/es_GlobalBan_Reason_db.txt', 'w')
    f.write(data)
    f.close()
    
# Class that will read the ban length list from the web site
class BanLengthList(threading.Thread):
  def run(self):
    f = urllib2.urlopen(websiteAddy + "index.php?page=getBanLengthList&es=1")
    data = f.read()
    data = data.strip() # remove any whitespace
    f.close()
    es.log('Done getting Ban Length list')

    # Open the es_GlobalBan_Length_db.txt db file to write to
    f = open(es.getAddonPath('GlobalBan') + '/es_GlobalBan_Length_db.txt', 'w')
    f.write(data)
    f.close()

def load():
  # Define the global values in this script
  global serverId, websiteAddy, banAppealMessage, hashCode, teachAdmins, clanName, allowAdminBanning, urlQueue

  es.log('Please wait 30 seconds while GlobalBan fully loads...')

  # Create a URL queue
  urlQueue = Queue.Queue()

  # Load the config file
  loadConfig()

  # Get the admin list
  gamethread.delayed(5, getAdminList)

  # Get the ban reason list
  gamethread.delayed(5, getBanReasonList)

  # Get the ban length list
  gamethread.delayed(5, getBanLengthList)

  # Now reload the keygroup
  gamethread.delayed(15, reloadLists)

  # Register the necessary commands for this script
  es.regsaycmd('!banmenu', 'GlobalBan/banReasonList', 'Ban the selected user')
  es.regcmd('gb_addReason', 'GlobalBan/addReason', 'Add a reason to the ban reason list')
  es.regcmd('gb_addBanLength', 'GlobalBan/addBanLength', 'Add a ban length to the ban length list')
  es.regcmd('gb_externalBanUser', 'GlobalBan/banExternal', 'Allow other scripts to add to the ban list')
  es.regcmd('cdb_addMember', 'GlobalBan/addMember', 'Used to add a new member to the keygroup on the fly')
  es.regcmd('gb_saveConfig', 'GlobalBan/saveConfig', 'Used to create the GlobalBan.cfg file from the web')
  es.regcmd('gb_loadConfig', 'GlobalBan/loadConfig', 'Used to load the GlobalBan.cfg file')

def loadConfig():
  global serverId, websiteAddy, banAppealMessage, hashCode, teachAdmins, clanName, allowAdminBanning
  es.log('Attempting to load values from GlobalBan.cfg')
  # Look for the GlobalBan config file and load the values from it
  if os.path.isfile(es.getAddonPath('GlobalBan') + '/GlobalBan.cfg'):
    es.mexec('../addons/eventscripts/GlobalBan/GlobalBan.cfg')

    serverId = es.ServerVar('serverId')
    websiteAddy = es.ServerVar('websiteAddy')
    banAppealMessage = es.ServerVar('banAppealMessage')
    hashCode = es.ServerVar('hash')
    teachAdmins = int(es.ServerVar('teachAdmins'))
    clanName = es.ServerVar('clanName')
    allowAdminBanning = es.ServerVar('allowAdminBanning')
  else:
    es.dbgmsg(0, 'GlobalBan: Unable to load GlobalBan.cfg! Please ensure it is in the ./GlobalBan/ directory.')

# Unset and delete specific variables when the script is unloaded
def unload():
  global urlQueue
  # We want to completely destroy the queue
  del urlQueue

# On player active (when the player connects to the server), check to see if they
# are allowed to play
def player_activate(event_var):
  if not es.isbot(event_var['userid']):
    ###############################################################################
    # TODO Add new user to a list so that they are only checked once during play
    ###############################################################################
    # Cannot pass event_var as it is invalid by the time validatePlayer executes
    # We delay the validation for 10 seconds in case the user's steam id is pending
    gamethread.delayed(1, validatePlayer, event_var['userid'])

# On admin death, display a message on how to bring up the ban menu
def player_death(event_var):
  if not es.isbot(event_var['userid']):
    if teachAdmins == 1:
      # This tells admins every time they die how to ban a player
      # This is to help them learn the command
      if isMember(event_var['es_steamid']):
        es.tell(event_var['userid'], '#multi', '#greenType the following in say to ban: #default!banmenu')

# Sync up with the web server every map change
def es_map_start(event_var):
  # Get the admin list
  gamethread.delayed(5, getAdminList)

  # Get the ban reason list
  gamethread.delayed(5, getBanReasonList)

  # Get the ban length list
  gamethread.delayed(5, getBanLengthList)

  # Now reload the keygroup
  gamethread.delayed(15, reloadLists)

# Determines if the person trying to use this script is an admin
def isMember(steamId):
  memberCheck = es.keygetvalue('clanMembers', steamId, 'member')
  if memberCheck == '1':
    return True
  else:
    return False

# Validates the player agains the mySQL database
def validatePlayer(playerid):
  global urlQueue
  
  if not es.isbot(playerid):
    playerName = es.getplayername(playerid)

    # Get IP address of the person
    player = playerlib.getPlayer(playerid)
    userIp = player.attributes["address"]
    
    # The webpage to check if the person is banned
    banCheckURL = websiteAddy + 'index.php?page=checkUser&es=1'
    banCheckURL += '&steamId=' + es.getplayersteamid(playerid)
    banCheckURL += '&serverId=' + serverId
    banCheckURL += '&hash=' + hashCode
    banCheckURL += '&name=' + urllib2.quote(playerName)
    banCheckURL += '&ip=' + userIp

    es.log('BAN CHECK URL:' + banCheckURL)

    # Put the ban check url into the thread queue
    urlQueue.put(banCheckURL)
    
    # We now want to process the Queue
    processUrlQueue()

def reloadLists():
  if es.exists('keygroup', 'clanMembers'):
    es.keygroupdelete('clanMembers')
  es.keygroupcreate('clanMembers')
  es.keygroupload('clanMembers', '|GlobalBan')
  
  if es.exists('keygroup', 'GlobalBan_Reason'):
    es.keygroupdelete('GlobalBan_Reason')
  es.keygroupcreate('GlobalBan_Reason')
  es.keygroupload('GlobalBan_Reason', '|GlobalBan')
  
  if es.exists('keygroup', 'GlobalBan_Length'):
    es.keygroupdelete('GlobalBan_Length')
  es.keygroupcreate('GlobalBan_Length')
  es.keygroupload('GlobalBan_Length', '|GlobalBan')

#def adminMenu():
# create a easy to use admin menu that will grab the data from the website

# This method brings up the ban reason menu, which is displayed when the admin invokes the
# !banmenu command
def banReasonList():
  # Get the userid of the person calling this menu
  playerid = es.getcmduserid()

  # Member check only needs to be performed on this menu
  if isMember(es.getplayersteamid(playerid)):
    if es.exists('keygroup', 'GlobalBan_Reason'):
      # Open the keygroup that contains all reason codes
      es.keygroupdelete('GlobalBan_Reason')
    es.keygroupload('GlobalBan_Reason', '|GlobalBan')

    # Create keymenu called banReasonMenu for user to select from
    banReasonMenu = keymenulib.create("banReasonMenu", "selectedBanReason", banLengthList, "GlobalBan_Reason", "#keyvalue reasonText", "#key", "Ban Reason List")
    banReasonMenu.send(playerid)
  else:
    es.tell(playerid, '#green', 'You are not an admin!')

# This method brings up the ban length menu, which is displayed after the admin selects
# a ban reason from the ban reason menu
def banLengthList(playerid, selectedBanReason, popupid):
  # Now set the ban reason selected to the correct admin in the admin keylist
  # Stored to the keylist so that it can be "passed" on
  # And to prevent other admins from stomping on each other
  es.keysetvalue('clanMembers', es.getplayersteamid(playerid), 'banReason', selectedBanReason)

  if es.exists('keygroup', 'GlobalBan_Length'):
    # Open the keygroup that contains all ban lengths
    es.keygroupdelete('GlobalBan_Length')
  es.keygroupload('GlobalBan_Length', '|GlobalBan')

  # Create keymenu called banLengthMenu for user to select from
  banLengthMenu = keymenulib.create("banLengthMenu", "selectedBanLength", banPlayerMenu, "GlobalBan_Length", "#keyvalue readable", "#key", "Ban Lengths")
  banLengthMenu.send(playerid)

# This method brings up the playerlist menu, which is displayed after the admin selects
# a ban length from the ban length menu
def banPlayerMenu(playerid, selectedBanLength, popupid):
  # Now set the ban length selected to the correct admin in the admin keylist
  # Stored to the keylist so that it can be "passed" on
  # And to prevent other admins from stomping on each other
  es.keysetvalue('clanMembers', es.getplayersteamid(playerid), 'banLength', selectedBanLength)

  if es.exists('keygroup', 'playerlist'):
    es.server.cmd('es_xkeygroupdelete playerlist')
  es.server.cmd('es_xcreateplayerlist playerlist')
  
  # Create keymenu called banmenu for admin to select a player from
  banmenu = keymenulib.create("banmenu", "userToBan", banInGame, "playerlist", "#keyvalue name", "#key", "Player List")
  banmenu.send(playerid)
  
  # Delete the playerlist
  es.server.cmd('es_xkeygroupdelete playerlist')
  

# This is the ban method used by this script
def banInGame(playerid, userToBan, popupid):
  callerSteamId = es.getplayersteamid(playerid)
  
  banReason = es.keygetvalue('clanMembers', callerSteamId, 'banReason')
  banLength = es.keygetvalue('clanMembers', callerSteamId, 'banLength')

  # No point banning if the selected person is really a bot
  if not es.isbot(userToBan):
    # Get the player's Steam ID that is being banned
    bannedSteamId = es.getplayersteamid(userToBan)

    # Get IP address of the person being banned
    player = playerlib.getPlayer(userToBan)
    bannedUserIp = player.attributes["address"]

    # Get the name of the player being banned
    nameOfBannedPlayer = es.getplayername(userToBan)

    # When timescale is set to "ignore" that means the length is actually the length ID
    banUser(playerid, bannedSteamId, banReason, banLength, "ignore", nameOfBannedPlayer, bannedUserIp)
  else:
    es.tell(callerId, '#green', 'You can not ban a bot')

# This method takes in 5 arguments and is used by external scripts wishing to tap into GlobalBan
def banExternal():
  callerId = es.getargv(1)
  bannedSteamId = es.getargv(2)
  banReason = es.getargv(3)
  banLength = es.getargv(4)
  timeScale = es.getargv(5)

  banUser(callerId, bannedSteamId, banReason, banLength, timeScale)

# This method will send the user's information to the website to be added to the ban database
# And it will kick the user from the server
def banUser(callerId, bannedSteamId, banReason, banLength, timeScale, nameOfBanned="", ipOfBanned=""):
  global urlQueue
  
  # Get the steam ID of the admin
  callerSteamId = es.getplayersteamid(callerId)

  # Get the name of the admin banning
  adminName = es.getplayername(callerId)

  # Make sure the one being banned is not a member
  # Unless the flag to allow the banning of admins is set
  if not isMember(bannedSteamId) or allowAdminBanning == 1:
    # Build the URL for processing the ban
    banUserURL = websiteAddy + 'index.php?page=processServerBan&es=1'
    banUserURL += '&steamId=' + bannedSteamId
    banUserURL += '&length=' + banLength
    banUserURL += '&timeScale=' + timeScale
    banUserURL += '&reason=' + banReason
    banUserURL += '&banner=' + callerSteamId
    banUserURL += '&serverId=' + serverId
    banUserURL += '&hash=' + hashCode
    banUserURL += '&name=' + urllib2.quote(nameOfBanned)
    banUserURL += '&ip=' + ipOfBanned
    banUserURL += '&bannerName=' + urllib2.quote(adminName)

    # Put the ban check url into the thread queue
    urlQueue.put(banUserURL)

    # Log the URL used for banning
    es.log('BAN USER URL:' + banUserURL)
    
    # We now want to process the Queue
    processUrlQueue()
    
    # Use the player object to kick the user with a ban appeal message
    es.server.cmd('es kickid "' + bannedSteamId + '" ' + banAppealMessage);
    
    # Tell the server that the player has been banned and log it
    es.msg('#lightgreen', nameOfBanned + ' has been banned from ALL ' + clanName + ' servers!')
    es.log(nameOfBanned + ' has been banned from ALL ' + clanName + ' servers!')
    es.log('Steam ID banned: ' + bannedSteamId)
  else:
    es.tell(callerId, '#green', 'You can not ban an admin')
  
    
def addReason():
  reasonId = es.getargv(1)
  reasonText = es.getargv(2)

  es.keycreate('GlobalBan_Reason', reasonId)
  es.keysetvalue('GlobalBan_Reason', reasonId, 'reasonId', reasonId)
  es.keysetvalue('GlobalBan_Reason', reasonId, 'reasonText', reasonText)
  
# This block allows for keys to be added to the key group
# This can be called from another es script or remotely from a webpage through rcon
def addMember():
  steamId = es.getargv(1)
  name = es.getargv(2)
  member = es.getargv(3)
  admin = es.getargv(4)

  es.keycreate('clanMembers', steamId)
  es.keysetvalue('clanMembers', steamId, 'name', name)
  es.keysetvalue('clanMembers', steamId, 'member', member)
  es.keysetvalue('clanMembers', steamId, 'admin', admin)

# This block recieves data from the web app and creates the GlobalBan.cfg file
# This may need to be split up if rcon can not send this much data or done a different way
def saveConfig():
  global serverId, websiteAddy, banAppealMessage, hashCode, teachAdmins, clanName, allowAdminBanning
  # Arguments
  p_serverId = es.getargv(1)
  p_websiteAddy = es.getargv(2)
  p_banAppealMessage = es.getargv(3)
  p_hash = es.getargv(4)
  p_teachAdmins = es.getargv(5)
  p_clanName = es.getargv(6)
  p_allowAdminBanning = es.getargv(7)
  
  serverId = p_serverId
  websiteAddy = p_websiteAddy
  banAppealMessage = p_banAppealMessage
  hashCode = p_hash
  teachAdmins = int(p_teachAdmins)
  clanName = p_clanName
  allowAdminBanning = p_allowAdminBanning

  # Open the GlobalBan.cfg file for writing
  cfg = open(es.getAddonPath('GlobalBan') + '/GlobalBan.cfg',"w")
  
  # Write the data to the cfg file
  cfg.write("serverId " + p_serverId)
  cfg.write("\n\n")
  cfg.write("websiteAddy \"" + p_websiteAddy + "\"")
  cfg.write("\n\n")
  cfg.write("banAppealMessage \"" + p_banAppealMessage + "\"")
  cfg.write("\n\n")
  cfg.write("hash \"" + p_hash + "\"")
  cfg.write("\n\n")
  cfg.write("teachAdmins " + p_teachAdmins)
  cfg.write("\n\n")
  cfg.write("clanName \"" + p_clanName + "\"")
  cfg.write("\n\n")
  cfg.write("allowAdminBanning " + p_allowAdminBanning)
  
  # Close the file
  cfg.close()
  
# This can be called from another es script or remotely from a webpage through rcon
def addBanLength():
  lengthId = es.getargv(1)
  length = es.getargv(2)
  timeScale = es.getargv(3)
  
  readable = length + ' ' + timeScale
  
  if length == '0':
    readable = 'Forever'

  if length == '1':
    if timeScale == 'minutes':
      readable = length + ' minute'
    if timeScale == 'hours':
      readable = length + ' hour'
    if timeScale == 'days':
      readable = length + ' day'
    if timeScale == 'weeks':
      readable = length + ' week'
    if timeScale == 'months':
      readable = length + ' month'

  es.keycreate('GlobalBan_Length', lengthId)
  es.keysetvalue('GlobalBan_Length', lengthId, 'length', length)
  es.keysetvalue('GlobalBan_Length', lengthId, 'timeScale', timeScale)
  es.keysetvalue('GlobalBan_Length', lengthId, 'readable', readable)
  
# This method is used to kick off threads to properly process the urlQueue
def processUrlQueue():
  # We hit the thread 5 times to make sure the queue is processed properly
  # This is quirky behavior by threads and is solved by this inelegant solution
  processUrl1 = runUrl()
  gamethread.delayed(1, processUrl1.start)
  processUrl2 = runUrl()
  gamethread.delayed(2, processUrl2.start)
  processUrl3 = runUrl()
  gamethread.delayed(3, processUrl3.start)
  processUrl4 = runUrl()
  gamethread.delayed(4, processUrl4.start)
  processUrl5 = runUrl()
  gamethread.delayed(5, processUrl5.start)

# We must kick off 5 threds for each list to make sure the list is retrieved
# Any less, and there is a chance it will not update properly
def getAdminList():
  # Get the admin list
  processUrl1 = AdminList()
  gamethread.delayed(1, processUrl1.start)
  processUrl2 = AdminList()
  gamethread.delayed(2, processUrl2.start)
  processUrl3 = AdminList()
  gamethread.delayed(3, processUrl3.start)
  processUrl4 = AdminList()
  gamethread.delayed(4, processUrl4.start)
  processUrl5 = AdminList()
  gamethread.delayed(5, processUrl5.start)
  
def getBanReasonList():
  # Get the ban reason list
  processUrl1 = BanReasonList()
  gamethread.delayed(1, processUrl1.start)
  processUrl2 = BanReasonList()
  gamethread.delayed(2, processUrl2.start)
  processUrl3 = BanReasonList()
  gamethread.delayed(3, processUrl3.start)
  processUrl4 = BanReasonList()
  gamethread.delayed(4, processUrl4.start)
  processUrl5 = BanReasonList()
  gamethread.delayed(5, processUrl5.start)
  
def getBanLengthList():
  # Get the ban length list
  processUrl1 = BanLengthList()
  gamethread.delayed(1, processUrl1.start)
  processUrl2 = BanLengthList()
  gamethread.delayed(2, processUrl2.start)
  processUrl3 = BanLengthList()
  gamethread.delayed(3, processUrl3.start)
  processUrl4 = BanLengthList()
  gamethread.delayed(4, processUrl4.start)
  processUrl5 = BanLengthList()
  gamethread.delayed(5, processUrl5.start)